"""
Raw Data Base module.

Module that provides Raw Data Base (RawDB) and the Shallow Copy (ShallowCopy)
classes.

Last Modified: $Id: rawdb.py 31 2005-05-31 20:02:02Z gustavo $

@author: U{Gustavo Sverzut Barbieri <barbieri@gmail.com>}
@license: GNU GPL
@copyright: (C) 2005 Gustavo Sverzut Barbieri
@version: $Rev: 31 $
"""









import cPickle as pickle
import threading



class RawDBInvalidId( Exception ):
    """Raw Data Base Invalid ID Exception.

    This exception is raised if a required ID was not present in DB yet.
    """
    def __init__( self, exception, id ):
	Exception.__init__( self )
	self.id = id
	self.exception = exception
    # __init__()

    def __str__( self ):
	return "%s: given id '%s' is not in data base." % \
	       ( self.__class__.__name__, self.id )
    # __str__()
# RawDBInvalidId


class RawDB( object ):
    """Raw Data Base operations.

    It can manage data, but doesn't handle locks or transactions.

    Creating a new Data Base and basic operations:

    >>> db = RawDB()
    >>> db.set( 10, 20 )
    >>> db.get( 10 )
    20
    >>> 10 in db
    True
    >>> 20 in db
    False


    Exceptions raised:

    >>> db.get( 20 )
    Traceback (most recent call last):
        ...
    RawDBInvalidId: RawDBInvalidId: given id '20' is not in data base.


    You can also delete items:

    >>> db.delete( 10 )
    >>> 10 in db
    False


    Alternative forms to set and get values (more pythonic):

    >>> db[ 10 ] = 100 # Same as db.set()
    >>> db[ 10 ]       # Same as db.get()
    100
    >>> del db[ 10 ]   # Same as db.delete()
    >>> 10 in db
    False
    >>> db[ 1 ] = 1


    You can check how many items are in the DB. If the Database is empty, it's
    considered False:

    >>> len( db )
    1
    >>> if db:         # tests if db is empty
    ...    print 'have items'
    ... else:
    ...    print 'empty'
    have items

    >>> db.clear()
    >>> if db:         # tests if db is empty
    ...    print 'have items'
    ... else:
    ...    print 'empty'
    empty


    To iterate over DB elements:

    >>> db[ 1 ] = 10
    >>> db[ 2 ] = 20
    >>> for k, v in db.iter_items():
    ...    print "%s: %s" % ( k, v )
    1: 10
    2: 20
    >>> print db.list_items()
    [(1, 10), (2, 20)]

    >>> db.clear()
    >>> db.update( { 10: 100, 20: 200, "a": "b" } )
    >>> l = db.list_items()
    >>> l.sort()
    >>> print l
    [(10, 100), (20, 200), ('a', 'b')]
    """
    def __init__( self ):
	self.__data = {}
	self.__lock = threading.RLock()
    # __init__()


    def load( self, file ):
        """Load data from C{file} into data base.

        File contents is a pickled dict.

        @param file: where to load data.
        @type  file: filename (L{str}) or L{file<__builtin__.file>} object.

        @see: L{pickle.load}
        """
	if not hasattr( file, "read" ) or not callable( file.read ):
	    file = open( file, "rb" )

	self.__lock.acquire()
	try:
	    self.__data = pickle.load( file )
	finally:
	    self.__lock.release()
    # load()


    def save( self, file ):
        """Save data from data base to C{file}.

        File contents will a pickled dict.

        @param file: where to save data.
        @type  file: filename (L{str}) or L{file<__builtin__.file>} object.

        @see: L{pickle.dump}
        """
	if not hasattr( file, "write" ) or not callable( file.write ):
	    file = open( file, "wb" )
	self.__lock.acquire()
	try:
	    pickle.dump( self.__data, file, -1 )
	finally:
	    self.__lock.release()
    # save()


    def get( self, id ):
        """Get value from C{id}.

        @requires: C{id} being in Data Base.

        @param id: an object in database.

        @return: value in database for object C{id}.
        @raise RawDBInvalidId: if C{id} is not in data base.
        """
        self.__lock.acquire()
        try:
            try:
                return self.__data[ id ]
            except KeyError, e:
                raise RawDBInvalidId( e, id )
        finally:
            self.__lock.release()
    # get()
    read = get
    __getitem__ = get


    def set( self, id, value ):
        """Set C{value} for C{id} in data base.

        @param id: any object to serve as identifier for C{value}.
        @param value: any object to be stored.
        """
	self.__lock.acquire()
	self.__data[ id ] = value
	self.__lock.release()
    # set()
    write = set
    __setitem__ = set


    def delete( self, id ):
        """Remove item referenced by C{id} from data base.

        @requires: C{id} being in Data Base.
        @postcondition: C{id} is not in data base anymore.
        """
	self.__lock.acquire()
        try:
            try:
                del self.__data[ id ]
            except KeyError, e:
                raise RawDBInvalidId( e, id )
        finally:
            self.__lock.release()
    # delete()
    __delitem__ = delete


    def is_in( self, id ):
        """Check if C{id} is in data base.

        @return: True if C{id} is in data base, False otherwise.
        @rtype: L{bool}
        """
	return id in self.__data
    # is_in()
    __contains__ = is_in


    def is_empty( self ):
        """Check if data base is empty.

        @return: True if data base is empty, False if it have items.
        @rtype: L{bool}
        """
	if self.__data:
	    return True
	else:
	    return False
    # is_empty()


    def __len__( self ):
	return len( self.__data )
    # __len__()


    def update( self, data ):
	"""Updates current database with data

	Data must be a dict, keys are ids.

	>>> db = RawDB()
	>>> print db
	RawDB:
	    <Empty>
	>>> db.update( { 1: 10, 2: 20 } )
	>>> print db
	RawDB:
	   1: 10
	   2: 20

        @postcondition: data base will have values in C{data}.

        @param data: dict with keys and values to be put into data base.
        @type  data: L{dict}

        @see: L{dict}
	"""
	self.__lock.acquire()
	self.__data.update( data )
	self.__lock.release()
    # update()


    def list_ids( self ):
	"""Returns a list of ids in this data base.

        @return: List of key/identifier objects.
        @rtype: L{list} of objects.
        """
	return self.__data.keys()
    # list_ids()


    def list_items( self ):
	"""Returns a list of elements in this data base

        @return: List of pairs C{(id, value)}
        @rtype: L{list} of L{tuple}s
	"""
	return self.__data.items()
    # list_items()


    def iter_ids( self ):
	"""Returns an iterator over ids in this data base.

        Iterator over key/identifier objects.
        @rtype: iterator
        """
	return self.__data.iterkeys()
    # iter_ids()


    def iter_items( self ):
	"""Returns an iterator over elements in this data base

        Iterator over pairs C{(id, value)} objects.
        @rtype: iterator over L{tuple}s
	"""
	return self.__data.iteritems()
    # iter_items()


    def clear( self ):
        """Clear data base contents.

        @postcondition: database is empty.
        """
	self.__lock.acquire()
	self.__data.clear()
	self.__lock.release()
    # clear()


    def __str__( self ):
	r = [ "%s:" % self.__class__.__name__ ]
	if self:
	    for k, v in self.iter_items():
		r.append( "   %s: %s" % ( k, v ) )
	else:
	    r.append( "    <Empty>" )
	return "\n".join( r )
    # __str__()
# RawDB







class ShallowCopy( RawDB ):
    """Shallow Copy a Data Base.

    Use this class to manipulate the database using a working copy, when you
    are done, just commit() your changes:

    Creating it:

    >>> db = RawDB()
    >>> sc = ShallowCopy( db )


    It should be able to access values from DB:

    >>> db[ 1 ] = 100
    >>> db[ 1 ]
    100
    >>> sc[ 1 ]
    100


    Values changed on the shallow copy doesn't affect the DB:

    >>> sc[ 1 ] = 200
    >>> db[ 1 ]
    100
    >>> sc[ 1 ]
    200


    Shallow Copy items have precedence over real data base:

    >>> sc.list_items()
    [(1, 200)]
    >>> sc.list_ids()
    [1]

    After you commit your changes, the DB is changed:

    >>> sc.commit()
    >>> db[ 1 ]
    200

    """
    def __init__( self, db ):
	RawDB.__init__( self )
	self.__db = db
	self.__lock = threading.Lock()
    # __init__()


    def get( self, id ):
	try:
	    return RawDB.get( self, id )
	except RawDBInvalidId:
	    return self.__db.get( id )
    # get()
    read = get
    __getitem__ = get


    def is_in( self, id ):
	return RawDB.is_in( self, id ) or self.__db.is_in( id )
    # is_in()
    __contains__ = is_in


    def update( self, data ):
	RawDB.update( self, data )
    # update()


    def list_ids( self ):
        return list( self.iter_ids() )
    # list_ids()


    def list_items( self ):
        return list( self.iter_items() )
    # list_items()


    def iter_ids( self ):
	for id in RawDB.iter_ids( self ):
            yield id
	for id in self.__db.iter_ids():
            if not RawDB.is_in( self, id ):
                yield id
    # iter_ids()


    def iter_items( self ):
	"""Returns an iterator over elements in this data base

	Elements are tuples in the format (id, value)
	"""
	for item in RawDB.iter_items( self ):
            yield item
	for item in self.__db.iter_items():
            # Do not yield values which are in shallow copy
            if not RawDB.is_in( self, item[ 0 ] ):
                yield item
    # iter_items()


    def commit( self ):
        """Commit data to real data base.

        @postcondition: data in shallow copy will be in data base.
        """
	self.__lock.acquire()
        data = {}
        for k, v in RawDB.iter_items( self ):
            data[ k ] = v
	self.__db.update( data )
	self.__lock.release()
    # commit()
# ShallowCopy




class singleton( object ):
    """Class that returns a singleton of RawDB.

    @note: This class, when instantiated returns an instance of
           RawDB.
    """
    db = RawDB()

    def __new__( self ):
	return self.db
    # __new__()
# singleton
